package cn.smvc.common.utils;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

/**
 * 将cron表达式解析 （1）可获得最近的时间
 * 
 * @author Ryan
 * 
 * 
 *         cron表达式是用来配置spring定时任务执行时间的字符串，由5个空格分隔成的6个域构成，格式如下： {秒} {分} {时} {日}
 *         {月} {周} 每一个域的含义解释：
 *         1)秒：表示在指定的秒数触发定时任务，范围0-59。例如，"*"表示任何秒都触发，"0,3"表示0秒和3秒触发。
 *         2)分：表示在指定的分钟触发定时任务，范围0-59。例如，"0-3"表示0分钟到3分钟每分钟都触发，"0/2"表示只有偶数分钟触发。
 *         3)时：表示在指定的小时触发定时任务，范围0-23。例如，"3-15/2"表示上午3点到下午3点每隔2个小时触发。
 *         4)日：表示在指定的日期触发定时任务，范围1-31(可以写0，但不会生效)。例如，"1"表示1号出发，"5,15"表示5号和15号出发。需要注意的是，日期可以写0，不会报错但也不会生效。
 *         5)月：表示在指定的月份触发定时任务，范围1-12。例如，"1-4,12"表示1月到4月以及12月触发。
 *         6)周：表示在指定的星期触发定时任务，范围0-7(0和7都表示周日)。例如，"?"表示一周都触发，"6,7"表示周六日触发。
 *         注意，1月到12月可以用对应的英文缩写JAN-DEC代替，周日到周六可以用对应的英文缩写SUN-SAT代替。但是，周日的缩写SUN只会被替换为0，因此在cron表达式的周域，我们可以写6-7，却不能写SAT-SUN。
 * 
 * 
 *         参考： http://blog.csdn.net/ukulelepku/article/details/54310035
 *         特殊字符的含义说明如下: 1)"*"：匹配该域的任意值，例如在日域上使用"*"，则表示每天都触发该定时任务。
 *         2)"?"：只能在日和周域使用，表示非明确的值，实际作用等同"*"，即匹配任意值。一般在日和周上会出现一次，当然，如果你对日和周两个域都使用"?"或者都使用其他值也没什么问题。
 *         3)"-"：表示范围，例如在分域上使用5-10表示从5分钟到10分钟每分钟触发一次。
 *         4)"/"：表示起始时间触发一次，然后每隔固定时间触发一次。例如，在分钟域使用"10/2"表示从10分钟开始每隔2分钟触发一次，直
 *         到58分钟。也可以和字符"-"连用，例如在分钟域使用"10-30/2"表示从10分钟开始每隔2分钟触发一次，直到30分钟。
 *         5)","：表示枚举多个值，这些值之间是"或"的关系。例如，在月份上使用"1-3,10,12"表示1月到3月，10月，12月都触发。
 *         下面是一些cron表达式和对应的含义： "0 15 10 ? * *" 每天上午10:15触发 "0 0/5 14 * * ?"
 *         在每天下午2点到下午2:55期间的每5分钟触发 "0 0-5 14 * * ?" 每天下午2点到下午2:05期间的每1分钟触发 "0
 *         10,44 14 ? 3 WED" 三月的星期三的下午2:10和2:44触发 "0 15 10 ? * MON-FRI"
 *         周一至周五的上午10:15触发
 */
public class CronSequenceGenerator {
	private final BitSet seconds = new BitSet(60);
	private final BitSet minutes = new BitSet(60);
	private final BitSet hours = new BitSet(24);
	private final BitSet daysOfWeek = new BitSet(7);
	private final BitSet daysOfMonth = new BitSet(31);
	private final BitSet months = new BitSet(12);
	private final String expression;
	private final TimeZone timeZone;
	
	public CronSequenceGenerator(String expression) {
		this(expression, TimeZone.getDefault());
	}
	
	public CronSequenceGenerator(String expression, TimeZone timeZone) {
		this.expression = expression;
		this.timeZone = timeZone;
		parse(expression);
	}

	/////////////////////////////////测试////////////////////////////////////////////
	private static void testCronAlg(Map<String, String> map) throws Exception {  
        int count = 0;  
        for (Map.Entry<String, String> entry : map.entrySet()) {  
            System.out.println(++count);  
            System.out.println("cron = "+entry.getKey());  
            System.out.println("date = "+entry.getValue());  
            CronSequenceGenerator cronSequenceGenerator = new CronSequenceGenerator(entry.getKey());  
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
            Date date = sdf.parse(entry.getValue());  
   
            long nanoTime1 = System.nanoTime();  
            Date date1 = null;  
            try {  
                date1 = cronSequenceGenerator.next(date);  
            } catch (Exception e) {  
            }  
            long nanoTime2 = System.nanoTime();  
            String str1 = null;  
            if (date1 != null) {  
                str1 = sdf.format(date1);  
            }  
            System.out.println("old method : result date = " + str1  
                    + " , consume " + (nanoTime2 - nanoTime1)/1000 + "us");  
   
   
            long nanoTime3 = System.nanoTime();  
            Date date2 = null;  
            try {  
                date2 = cronSequenceGenerator.next(date);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            long nanoTime4 = System.nanoTime();  
            String str2 = null;  
            if (date2 != null) {  
                str2 = sdf.format(date2);  
            }  
            System.out.println("new method : result date = " + str2  
                    + " , consume " + (nanoTime4 - nanoTime3)/1000 + "us");  
        }  
    }  
	public static void main(String[] args) {
//		String cron = "0 15 10 ? * *";
//		CronSequenceGenerator cronParse = new CronSequenceGenerator(cron);
//		cronParse.parse(cron);
		Map<String, String> map = new HashMap<>();  
        map.put("0 0 8 * * *", "2011-03-25 13:22:43");  
        map.put("0/2 1 * * * *", "2016-12-25 18:00:45");  
        map.put("0 0/5 14,18 * * ?", "2016-01-29 04:01:12");  
        map.put("0 15 10 ? * MON-FRI", "2022-08-31 23:59:59");  
        map.put("0 26,29,33 * * * ?", "2013-09-12 03:04:05");  
        map.put("10-20/4 10,44,30/2 10 ? 3 WED", "1999-10-18 12:00:00");  
        map.put("0 0 0 1/2 MAR-AUG ?", "2008-09-11 19:19:19");  
        map.put("0 10-50/3,57-59 * * * WED-FRI", "2003-02-09 06:17:19");  
        map.put("0/2 0 1 29 2 FRI ", "2016-05-23 09:13:53");  
        map.put("0/2 0 1 29 2 5 ", "2016-05-23 09:13:53");  
        map.put("0 10,44 14 ? 3 WED", "2016-12-28 19:01:35");  
        try {
			testCronAlg(map);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
	}

	public Map<String, Object> parse(String cron) {
//		System.out.println("cron=======>" + cron);
		Map<String, Object> result = new HashMap<String, Object>();
		String[] cronString = StringUtils.split(cron.trim(), " ");
		if (cronString.length == 6) {
			if (cronString[0] != null && cronString[0].length() > 0) {
				setNumberHits(seconds, cronString[0], 0, 60);
			}
			if (cronString[1] != null && cronString[1].length() > 0) {
				setNumberHits(minutes, cronString[1], 0, 60);
			}
			if (cronString[2] != null && cronString[2].length() > 0) {
				setNumberHits(hours, cronString[2], 0, 24);
			}
			if (cronString[3] != null && cronString[3].length() > 0) {
				setDaysOfMonth(daysOfMonth, cronString[3]);
			}
			if (cronString[4] != null && cronString[4].length() > 0) {
				setMonths(months, cronString[4]);
			}
			if (cronString[5] != null && cronString[5].length() > 0) {
				setDayOfWeek(daysOfWeek, cronString[5]);
			}
		} else {
			throw new IllegalArgumentException(String.format(
					"Cron expression must consist of 6 fields (found %d in \"%s\")", cronString.length, expression));
		}
//		System.out.println("seconds==>" + seconds.toString());
//		System.out.println("minutes==>" + minutes.toString());
//		System.out.println("hours==>" + hours.toString());
//		System.out.println("daysOfMonth==>" + daysOfMonth.toString());
//		System.out.println("months==>" + months.toString());
//		System.out.println("daysOfWeek==>" + daysOfWeek.toString());
		return result;
	}

	private void setNumberHits(BitSet bits, String value, int min, int max) {
		String[] fields = StringUtils.split(value, ",");
		for (String field : fields) {
			if (!field.contains("/")) {
				// Not an incrementer so it must be a range (possibly empty)
				int[] range = getRange(field, min, max);
				bits.set(range[0], range[1] + 1);
			} else {
				String[] split = StringUtils.split(field, "/");
				if (split.length > 2) {
					throw new IllegalArgumentException("Incrementer has more than two fields: '" + field
							+ "' in expression \"" + this.expression + "\"");
				}
				int[] range = getRange(split[0], min, max);
				if (!split[0].contains("-")) {
					range[1] = max - 1;
				}
				int delta = Integer.valueOf(split[1]);
				for (int i = range[0]; i <= range[1]; i += delta) {
					bits.set(i);
				}
			}
		}
	}

	private int[] getRange(String field, int min, int max) {
		int[] result = new int[2];
		if (field.contains("*")) {
			result[0] = min;
			result[1] = max - 1;
			return result;
		}
		if (!field.contains("-")) {
			result[0] = result[1] = Integer.valueOf(field);
		} else {
			String[] split = StringUtils.split(field, "-");
			if (split.length > 2) {
				throw new IllegalArgumentException(
						"Range has more than two fields: '" + field + "' in expression \"" + this.expression + "\"");
			}
			result[0] = Integer.valueOf(split[0]);
			result[1] = Integer.valueOf(split[1]);
		}
		if (result[0] >= max || result[1] >= max) {
			throw new IllegalArgumentException(
					"Range exceeds maximum (" + max + "): '" + field + "' in expression \"" + this.expression + "\"");
		}
		if (result[0] < min || result[1] < min) {
			throw new IllegalArgumentException(
					"Range less than minimum (" + min + "): '" + field + "' in expression \"" + this.expression + "\"");
		}
		return result;
	}

	/**
	 * 对于日期，先将该域的子cron表达式中出现的字符"?"替换成"*"，然后使用基础解析算法进行处理。
	 * 日期的范围是1-31，因此位数组的第0位是用不到的，在基础解析算法之后进行清除。
	 * 位数组的第0位最后会清除。
	 * @param bits
	 * @param field
	 */
	private void setDaysOfMonth(BitSet bits, String field) {
		int max = 31;
		// Days of month start with 1 (in Cron and Calendar) so add one
		setDays(bits, field, max + 1);
		// ... and remove it from the front
		bits.clear(0);
	}

	private void setDays(BitSet bits, String field, int max) {
		if (field.contains("?")) {
			field = "*";
		}
		setNumberHits(bits, field, 0, max);
	}
	
	/**
	 * 对于月份，先将该域的英文缩写JAN-DEC替换成对应的数字(1-12)，然后使用基础解析算法进行处理。
	 * 但是由于cron表达式中配置的月份范围是1-12，Calendar中的月份范围是0-11，
	 * 所以为了后续算法使用方便，在基础解析算法处理完之后将months位数组整体左移1位。
	 * @param bits
	 * @param value
	 */
	private void setMonths(BitSet bits, String value) {  
	   int max = 12;  
	   value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC");  
	   BitSet months = new BitSet(13);  
	   // Months start with 1 in Cron and 0 in Calendar, so push the values first into a longer bit set  
	   setNumberHits(months, value, 1, max + 1);  
	   // ... and then rotate it to the front of the months  
	   for (int i = 1; i <= max; i++) {  
	      if (months.get(i)) {  
	         bits.set(i - 1);  
	      }  
	   }  
	}
	
	private String replaceOrdinals(String f, String arrStr) {
		String[] list = StringUtils.split(arrStr, ",");
		f = f.toUpperCase();
		for (int i = 0; i < list.length; i++) {
			String item = list[i].toUpperCase();
			if (f.contains(item)) {				
				f = f.replaceAll(item, i + "");
			}
		}
		return f;
	}
	/**
	 * 对于星期，先将该域的英文缩写SUN-SAT替换成对应的数字(0-6)，接着将该域中的字符"?"替换成"*"，
	 * 然后使用基础解析算法处理。最后，由于周日对应的值有两个0和7，
	 * 因此对daysOfWeek位数组的第0位和第7位取或，将结果保存到第0位，并清除第7位。
	 * @param bits
	 * @param field
	 */
	private void setDayOfWeek(BitSet bits, String field) {
		field = replaceOrdinals(field, "SUN,MON,TUE,WED,THU,FRI,SAT");
		if (field.contains("?")) {  
			field = "*";  
		}  
		setNumberHits(bits, field, 0, 8);
		if (this.daysOfWeek.get(7)) {  
		   // Sunday can be represented as 0 or 7  
		   this.daysOfWeek.set(0);  
		   this.daysOfWeek.clear(7);  
		}  
	}
	
	///////////////////////////////////////////////////////////////////////////////////
	public Date next(Date date) {
		/*
		The plan:

		1 Round up to the next whole second

		2 If seconds match move on, otherwise find the next match:
		2.1 If next match is in the next minute then roll forwards

		3 If minute matches move on, otherwise find the next match
		3.1 If next match is in the next hour then roll forwards
		3.2 Reset the seconds and go to 2

		4 If hour matches move on, otherwise find the next match
		4.1 If next match is in the next day then roll forwards,
		4.2 Reset the minutes and seconds and go to 2

		...
		*/

		Calendar calendar = new GregorianCalendar();
		calendar.setTimeZone(this.timeZone);
		calendar.setTime(date);

		// First, just reset the milliseconds and try to calculate from there...
		calendar.set(Calendar.MILLISECOND, 0);
		long originalTimestamp = calendar.getTimeInMillis();
//		doNext(calendar, calendar.get(Calendar.YEAR));
		doNextNew(calendar);

		if (calendar.getTimeInMillis() == originalTimestamp) {
			// We arrived at the original timestamp - round up to the next whole second and try again...
			calendar.add(Calendar.SECOND, 1);
//			doNext(calendar, calendar.get(Calendar.YEAR));
			doNextNew(calendar);
		}

		return calendar.getTime();
	}
	
	//从calendar开始寻找下一个匹配cron表达式的时间  
	private void doNextNew(Calendar calendar) {  
	    //calendar中比当前更高的域是否调整过  
	    boolean changed = false;  
	    List<Integer> fields = Arrays.asList(Calendar.MONTH, Calendar.DAY_OF_MONTH,  
	            Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND);  
	   
	    //依次调整月，日，时，分，秒  
	    for (int field : fields) {  
	        if (changed) {  
	            calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0);  
	        }  
	        if (!checkField(calendar, field)) {  
	            changed = true;  
	            findNext(calendar, field);  
	        }  
	    }  
	}  
	   
	//检查某个域是否匹配cron表达式  
	private boolean checkField(Calendar calendar, int field) {  
	    switch (field) {  
	        case Calendar.MONTH: {  
	            int month = calendar.get(Calendar.MONTH);  
	            return this.months.get(month);  
	        }  
	        case Calendar.DAY_OF_MONTH: {  
	            int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);  
	            int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1;  
	            return this.daysOfMonth.get(dayOfMonth) && this.daysOfWeek.get(dayOfWeek);  
	        }  
	        case Calendar.HOUR_OF_DAY: {  
	            int hour = calendar.get(Calendar.HOUR_OF_DAY);  
	            return this.hours.get(hour);  
	        }  
	        case Calendar.MINUTE: {  
	            int minute = calendar.get(Calendar.MINUTE);  
	            return this.minutes.get(minute);  
	        }  
	        case Calendar.SECOND: {  
	            int second = calendar.get(Calendar.SECOND);  
	            return this.seconds.get(second);  
	        }  
	        default:  
	            return true;  
	    }  
	}  
	   
	//调整某个域到下一个匹配值，使其符合cron表达式  
	private void findNext(Calendar calendar, int field) {
	    switch (field) {  
	        case Calendar.MONTH: {  
	            if (calendar.get(Calendar.YEAR) > 2099) {  
	                throw new IllegalArgumentException("year exceeds 2099!");  
	            }  
	            int month = calendar.get(Calendar.MONTH);  
	            int nextMonth = this.months.nextSetBit(month);  
	            if (nextMonth == -1) {  
	                calendar.add(Calendar.YEAR, 1);  
	                calendar.set(Calendar.MONTH, 0);  
	                nextMonth = this.months.nextSetBit(0);  
	            }  
	            if (nextMonth != month) {  
	                calendar.set(Calendar.MONTH, nextMonth);  
	            }  
	            break;  
	        }  
	        case Calendar.DAY_OF_MONTH: {  
	            while (!this.daysOfMonth.get(calendar.get(Calendar.DAY_OF_MONTH))  
	                    || !this.daysOfWeek.get(calendar.get(Calendar.DAY_OF_WEEK) - 1)) {  
	                int max = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);  
	                int nextDayOfMonth = this.daysOfMonth.nextSetBit(calendar.get(Calendar.DAY_OF_MONTH) + 1);  
	                if (nextDayOfMonth == -1 || nextDayOfMonth > max) {  
	                    calendar.add(Calendar.MONTH, 1);  
	                    findNext(calendar, Calendar.MONTH);  
	                    calendar.set(Calendar.DAY_OF_MONTH, 1);  
	                } else {  
	                    calendar.set(Calendar.DAY_OF_MONTH, nextDayOfMonth);  
	                }  
	            }  
	            break;  
	        }  
	        case Calendar.HOUR_OF_DAY: {  
	            int hour = calendar.get(Calendar.HOUR_OF_DAY);  
	            int nextHour = this.hours.nextSetBit(hour);  
	            if (nextHour == -1) {  
	                calendar.add(Calendar.DAY_OF_MONTH, 1);  
	                findNext(calendar, Calendar.DAY_OF_MONTH);  
	                calendar.set(Calendar.HOUR_OF_DAY, 0);  
	                nextHour = this.hours.nextSetBit(0);  
	            }  
	            if (nextHour != hour) {  
	                calendar.set(Calendar.HOUR_OF_DAY, nextHour);  
	            }  
	            break;  
	        }  
	        case Calendar.MINUTE: {  
	            int minute = calendar.get(Calendar.MINUTE);  
	            int nextMinute = this.minutes.nextSetBit(minute);  
	            if (nextMinute == -1) {  
	                calendar.add(Calendar.HOUR_OF_DAY, 1);  
	                findNext(calendar, Calendar.HOUR_OF_DAY);  
	                calendar.set(Calendar.MINUTE, 0);  
	                nextMinute = this.minutes.nextSetBit(0);  
	            }  
	            if (nextMinute != minute) {  
	                calendar.set(Calendar.MINUTE, nextMinute);  
	            }  
	            break;  
	        }  
	        case Calendar.SECOND: {  
	            int second = calendar.get(Calendar.SECOND);  
	            int nextSecond = this.seconds.nextSetBit(second);  
	            if (nextSecond == -1) {  
	                calendar.add(Calendar.MINUTE, 1);  
	                findNext(calendar, Calendar.MINUTE);  
	                calendar.set(Calendar.SECOND, 0);  
	                nextSecond = this.seconds.nextSetBit(0);  
	            }  
	            if (nextSecond != second) {  
	                calendar.set(Calendar.SECOND, nextSecond);  
	            }  
	            break;  
	        }  
	    }  
	} 

}
